﻿/* ****************************************************************************
 *
 * Copyright (c) Francesco Abbruzzese. All rights reserved.
 * francesco@dotnet-programming.com
 * http://www.dotnet-programming.com/
 * 
 * This software is subject to a dual license model 
 * included in the license.txt file of this distribution.
 * 
 * You must not remove this notice, or any other, from this software.
 *
 * ***************************************************************************/
using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using System.Dynamic;
using System.ComponentModel;
using System.Reflection;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Collections.ObjectModel;


namespace BindToolkit
{
    public partial class BindWrapper:DynamicObject, INotifyPropertyChanged, IDataErrorInfo
    {
        protected object WrappedObject { get; set; }
        protected Type WrappedType { get; set; }
        protected Dictionary<string, BindWrapper> wrappers;
        protected Dictionary<string, object> collections;
        public BindWrapper(object wrappedObject, bool wrapDeep=false, bool wrapEnumerables=false):base()
        {
            if (wrappedObject == null) throw new ArgumentNullException("wrappedObject");
            WrappedObject = wrappedObject;
            WrappedType = wrappedObject.GetType();
            if (wrapDeep) 
                recursiveWrapping(wrapEnumerables);
            
        }     
        public BindWrapper(Type wrappedType):base()
        {
            if (wrappedType == null) throw new ArgumentNullException("wrappedType");
            ConstructorInfo ci=wrappedType.GetConstructor(new Type[0]);
            if (ci == null) throw new ArgumentException(string.Format(Resource.No0ArgumentsConstructor, wrappedType), "wrappedType");
            WrappedObject = wrappedType.GetConstructor(new Type[0]).Invoke(new object[0]);
            WrappedType = wrappedType;
        }

       

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            
            return TryGetMember(binder.Name, out result);
        }
        protected  bool TryGetMember(string name, out object result)
        {

            result = null;

            if (wrappers != null && wrappers.ContainsKey(name))
            {
                result = wrappers[name];
                return true;
            }
            if (collections != null && collections.ContainsKey(name))
            {
                result = collections[name];
                return true;
            }
            PropertyInfo pi = WrappedType.GetProperty(name, new Type[0]);
            if (pi == null)
            {

                return true;
            }
            result = pi.GetValue(WrappedObject, new object[0]);
            return true;
        }
        public override bool TryConvert(ConvertBinder binder, out object result)
        {
            if (binder.ReturnType == WrappedType) {
                result = WrappedObject;
                return true;
            }
            result = null;
            return  false;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            PropertyInfo pi = WrappedType.GetProperty(binder.Name, new Type[0]);
            bool changed = false;
            if (value != null)
            {
                if (value.GetType() == typeof(BindWrapper))
                {
                    if (wrappers == null) wrappers = new Dictionary<string, BindWrapper>();
                    wrappers[binder.Name] = value as BindWrapper;
                    value = (value as BindWrapper).WrappedObject;
                    changed = true;
                }
                if (value.GetType().GetInterface("System.Collections.IEnumerable", false) != null && value.GetType() != typeof(string) && (pi == null || !pi.PropertyType.IsAssignableFrom(value.GetType())))
                {
                    AddToCollections(binder.Name, value);
                }
                
            }
            else
            {
                if (wrappers != null && wrappers.ContainsKey(binder.Name))
                {
                    wrappers.Remove(binder.Name);
                    changed = true;
                }
            }
            if (pi != null)
            {
                if (value != null && !pi.PropertyType.IsAssignableFrom(value.GetType()))
                {
                    IConvertible conv = value as IConvertible;
                    if (conv != null)
                    {
                        object prev = pi.GetValue(WrappedObject, new object[0]);
                        object newest = null;
                        try
                        {
                            newest = conv.ToType(pi.PropertyType, CultureInfo.CurrentUICulture);
                        }
                        catch(Exception e)
                        {
                            string error = getFormatError(pi, value);
                            if (value == null || (value as string) == string.Empty || value == System.Windows.DependencyProperty.UnsetValue)
                            {
                                if (!pi.PropertyType.IsValueType)
                                {
                                    newest = null;
                                }
                                else
                                {
                                    newest = Activator.CreateInstance(pi.PropertyType);
                                }
                            }
                            else if (error != null)
                            {
                                throw (new FormatException(error));

                            }
                            else if (value.GetType() == typeof(string))
                                throw (e);
                            else
                                throw new InvalidCastException(string.Format(Resource.UnableToConvert, value.GetType().ToString(), pi.PropertyType.ToString()));
                        }
                        changed = prev != newest;
                        pi.SetValue(WrappedObject, newest, new object[0]);
                    }
                    else
                        throw new InvalidCastException(string.Format(Resource.UnableToConvert, value.GetType().ToString(), pi.PropertyType.ToString()));

                }
                else
                {

                    object prev = pi.GetValue(WrappedObject, new object[0]);
                    changed = prev != value;
                    pi.SetValue(WrappedObject, value, new object[0]);
                }
                
            }
            if (PropertyChanged != null && changed)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(binder.Name));
            }
            if (changed) triggerDependencies(binder.Name);
            return true;

        }
        
        private List<ValidationException> remoteErrors = null;
        protected void DeclareRemoteError(ValidationException validationException)
        {
            if (remoteErrors == null) remoteErrors = new List<ValidationException>();
            remoteErrors.Add(validationException);
            
            
        }
        protected void SetAllExceptions(IEnumerable<ValidationException> remoteErrors)
        {
            foreach (ValidationException remoteError in remoteErrors)
            {
                foreach (string member in remoteError.ValidationResult.MemberNames)
                {
                    AddError(
                        member, 
                        remoteError.ValidationResult.ErrorMessage,
                        new ValidationException(
                        new ValidationResult(remoteError.ValidationResult.ErrorMessage, new string[]{member}),
                        remoteError.ValidationAttribute,
                        remoteError.Value));
                }
            }
        }
        protected void AddError(string propertyName, string error, ValidationException remoteError)
        {
            bool handled = propagateDown(propertyName, error, remoteError);

            if (!handled)
            {
                //(remoteError.ValidationResult.MemberNames as string[])[0] = string.Empty;
                DeclareRemoteError(remoteError);
            }

        }

        private bool propagateDown(string propertyName, string error, ValidationException remoteError)
        {
            if (string.IsNullOrWhiteSpace(propertyName )) return false ;

            int pos = propertyName.IndexOf('.');
            int pos1 = propertyName.IndexOf('[');
            string field = null;
            string tail = null;
            if (pos < 0)
            {
                if (pos1 < 0)
                {
                    field = propertyName;
                }
                else
                {
                    field = propertyName.Substring(0, pos1);
                    tail = propertyName.Substring(pos1);
                }
            }
            else
            {
                if (pos1 < 0)
                {
                    field = propertyName.Substring(0, pos);
                    if (propertyName.Length > pos + 1)
                        tail = propertyName.Substring(pos + 1);
                }
                else
                {
                    if (pos < pos1)
                    {
                        field = propertyName.Substring(0, pos);
                        if (propertyName.Length > pos + 1)
                            tail = propertyName.Substring(pos + 1);
                    }
                    else
                    {
                        field = propertyName.Substring(0, pos1);
                        tail = propertyName.Substring(pos1);
                    }
                }
            }
           
            PropertyInfo pi = WrappedType.GetProperty(field);

            object propertyValue = null;
            bool res = TryGetMember(field, out propertyValue);
            if (!res) return false;
            if (propertyValue != null )
            {
                if (propertyValue.GetType() == typeof(BindWrapper))
                {

                    (propertyValue as BindWrapper).AddError(tail, error, remoteError);
                    return true;
                }
                else
                {
                    IEnumerable en = propertyValue as IEnumerable;
                    if (en == null || propertyValue is string) return false;
                    else return propagateDownIndices(en, tail, error, remoteError);
                }
            }
            else
            {
                return false;
            }
        }
        private bool propagateDownIndices(IEnumerable en, string tail, string error, ValidationException remoteError)
        {
            if (tail.Length == 0) return false;
            if (tail[0] != '[') return false;
            int pos = tail.IndexOf(']');
            if (pos < 0) return false;
            string index = tail.Substring(1, pos - 1).Trim();


            if (tail.Length > pos + 1) tail = tail.Substring(pos + 1);
            else tail = string.Empty;
            object nextLevel = null;

            if (!TryExtractItem(en, index, ref nextLevel)) return false;

            if (nextLevel == null) return false;
            if (nextLevel.GetType() == typeof(BindWrapper))
            {
                (nextLevel as BindWrapper).AddError(tail.Substring(1), error, remoteError);
                return true;
            }
            else
            {
                en = nextLevel as IEnumerable;
                if (en == null || nextLevel is string) return false;
                else return propagateDownIndices(en, tail.Trim(), error, remoteError);
            }
        }
        private bool TryExtractItem(IEnumerable en, string key, ref object result)
        {
            result = null;
            Type t = en.GetType();
            //try integer indexer
            PropertyInfo pi = null;
            try { pi = t.GetProperty("Item"); }
            catch { };
            if (pi == null) return false;

            long longIndex = 0;
            bool isLong = long.TryParse(key, out longIndex);
            int intIndex = 0;
            bool isInt = int.TryParse(key, out intIndex);

            if (isInt)
            {
                try { result = pi.GetValue(en, new object[] { intIndex }); }
                catch { }
                if (result != null) return true;
            }
            else if (isLong)
            {
                try { result = pi.GetValue(en, new object[] { intIndex }); }
                catch { }
                if (result != null) return true;
            }
            else
            {
                try { result = pi.GetValue(en, new object[] { key }); }
                catch { }
                if (result != null) return true;
            }
            return false;
        }
        protected void ResetAllExceptions()
        {
            remoteErrors = null;
            if (wrappers != null)
            {
                foreach (string key in wrappers.Keys)
                {
                    BindWrapper bw = wrappers[key];
                    if (bw != null) bw.ResetAllExceptions();
                }
            }
        }
        
        
        
        
        public string Error
        {
            get
            {
                if (!globalEvaluationFired) return string.Empty;
                List<ValidationResult> errors = new List<ValidationResult>();
                bool success = Validator.TryValidateObject(WrappedObject, new ValidationContext(WrappedObject, null, null), errors);
                if (remoteErrors != null)
                {
                    success = false;
                    foreach(ValidationException remoteError in remoteErrors)
                        errors.Add(remoteError.ValidationResult);
                }

                if (success) return null;
                else
                {
                    objectValid = false;
                    return getErrorString(errors);
                }
                
            }
        }

        protected string getErrorString(List<ValidationResult> errors)
        {
            StringBuilder sb = new StringBuilder();
            foreach (ValidationResult vr in errors)
            {
                if (sb.Length > 0)
                    sb.Append(Environment.NewLine);
                sb.Append(vr.ErrorMessage);

            }
            return sb.ToString();
        }
        private bool globalEvaluationFired=false;
        public string this[string columnName]
        {
            get
            {
                if (columnName == "Error") return string.Empty;
                object result = null;
                PropertyInfo pi = WrappedType.GetProperty(columnName, new Type[0]);
                
                 result = pi.GetValue(WrappedObject, new object[0]);
               
                IDataErrorInfo er = result as IDataErrorInfo;
                if (er == null && wrappers != null && wrappers.ContainsKey(columnName)) er = wrappers[columnName] as IDataErrorInfo;
                if (er != null && !globalEvaluationFired) return string.Empty;
                ValidationContext ctx = new ValidationContext(WrappedObject, null, null);
                ctx.MemberName=columnName;
                List<ValidationResult> errors = new List<ValidationResult>();
                Validator.TryValidateProperty(result, ctx, errors);
                bool success = true;
                foreach (ValidationResult res in errors)
                {
                    if (res != ValidationResult.Success && (!string.IsNullOrEmpty(res.ErrorMessage)) ) success = false;
                }
                handlesValidationDependencies(columnName, errors);
                string otherErrors = null;
                if (er != null) otherErrors = er.Error;
                success = success && string.IsNullOrEmpty(otherErrors);
                if (success) return null;
                objectValid = false;
                if (string.IsNullOrEmpty(otherErrors))
                    return getErrorString(errors);
                else
                    return
                        getErrorString(errors) + Environment.NewLine + otherErrors;
            }
        }
        public bool objectValid = true;
        public bool ValidateWholeObject(IEnumerable<ValidationException> exceptions=null)
        {
            ResetGlobalEvaluation();
            if (exceptions != null)
            {
                SetAllExceptions(exceptions);
            }
            return validateWholeObjectRec();
        }
        public bool AddRemoteErrors(IEnumerable<ValidationException> remoteErrors)
        {
            if (remoteErrors != null)
            {
                SetAllExceptions(remoteErrors);
            }
            return validateWholeObjectRec();
        }
        protected bool validateNestedEnumerables(object enumerable)
        {
            bool result = true;
            IDictionary<string, BindWrapper> leafDictionary = enumerable as IDictionary<string, BindWrapper>;
            if (leafDictionary != null)
            {
                foreach (string key in leafDictionary.Keys)
                {
                    BindWrapper bw = leafDictionary[key];
                    if (bw != null) result = bw.validateWholeObjectRec() && result;
                }
                return result;
            }//may be not leaf or simply mixed
            IDictionary<string, object> notLeafDictionary = enumerable as IDictionary<string, object>;
            if (notLeafDictionary != null)
            {
                foreach (string key in notLeafDictionary.Keys)
                {
                    object ob = notLeafDictionary[key];
                    if (ob != null)
                    {
                        BindWrapper bw = ob as BindWrapper;
                        if (bw != null) result = bw.validateWholeObjectRec() && result;
                        else result = validateNestedEnumerables(ob) && result;
                    }
                }
                return result;
            }
            IEnumerable<BindWrapper> leafEnumerable = enumerable as IEnumerable<BindWrapper>;
            if (leafEnumerable != null)
            {
                foreach (BindWrapper bw in leafEnumerable)
                {
                    if (bw != null) result = bw.validateWholeObjectRec() && result;
                }
                return result;
            }//may be not leaf or simply mixed
            IEnumerable<object> notLeafEnumerable = enumerable as IEnumerable<object>;
            if (notLeafEnumerable != null)
            {
                foreach (object ob in notLeafEnumerable)
                {
                    if (ob != null)
                    {
                        BindWrapper bw = ob as BindWrapper;
                        if (bw != null) result = bw.validateWholeObjectRec() && result;
                        else result = validateNestedEnumerables(ob) && result;
                    }
                }
                return result;
            }
            return result;
        }
        protected bool  validateWholeObjectRec()
        {
            objectValid = true;
            globalEvaluationFired = true;
            
                
            if (wrappers != null)
            {
                foreach (string key in wrappers.Keys)
                {
                    BindWrapper bw = wrappers[key];
                    if (bw != null)
                    {
                        if (!bw.validateWholeObjectRec()) objectValid = false;
                    }
                }
            }
            if (collections != null)
            {
                foreach (string key in collections.Keys)
                {
                    if(!validateNestedEnumerables(collections[key])) objectValid = false;
                }
            }
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(System.Windows.Data.Binding.IndexerName));
                PropertyChanged(this, new PropertyChangedEventArgs("Error"));
            }
            return objectValid;
        }

        protected void resetNestedEnumerables(object enumerable)
        {
            
            IDictionary<string, BindWrapper> leafDictionary = enumerable as IDictionary<string, BindWrapper>;
            if (leafDictionary != null)
            {
                foreach (string key in leafDictionary.Keys)
                {
                    BindWrapper bw = leafDictionary[key];
                    if (bw != null) bw.ResetGlobalEvaluation();
                }
                return ;
            }//may be not leaf or simply mixed
            IDictionary<string, object> notLeafDictionary = enumerable as IDictionary<string, object>;
            if (notLeafDictionary != null)
            {
                foreach (string key in notLeafDictionary.Keys)
                {
                    object ob = notLeafDictionary[key];
                    if (ob != null)
                    {
                        BindWrapper bw = ob as BindWrapper;
                        if (bw != null) bw.ResetGlobalEvaluation();
                        else resetNestedEnumerables(ob);
                    }
                }
                return ;
            }
            IEnumerable<BindWrapper> leafEnumerable = enumerable as IEnumerable<BindWrapper>;
            if (leafEnumerable != null)
            {
                foreach (BindWrapper bw in leafEnumerable)
                {
                    if (bw != null) bw.ResetGlobalEvaluation();
                }
                return;
            }//may be not leaf or simply mixed
            IEnumerable<object> notLeafEnumerable = enumerable as IEnumerable<object>;
            if (notLeafEnumerable != null)
            {
                foreach (object ob in notLeafEnumerable)
                {
                    if (ob != null)
                    {
                        BindWrapper bw = ob as BindWrapper;
                        if (bw != null) bw.ResetGlobalEvaluation();
                        resetNestedEnumerables(ob);
                    }
                }
                return;
            }
            return;
        }

        public void ResetGlobalEvaluation()
        {
            globalEvaluationFired = false;
            remoteErrors = null;
            if (wrappers != null)
            {
                foreach (string key in wrappers.Keys)
                {
                    BindWrapper bw = wrappers[key];
                    if (bw != null)
                    {
                        bw.ResetGlobalEvaluation();
                    
                    }
                }
            }
            if (collections != null)
            {
                foreach (string key in collections.Keys)
                {
                    resetNestedEnumerables(collections[key]); 
                }
            }
        }
        private void controlPropertyErrors(string property)
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }
}
